<!DOCTYPE html>
<html lang="en">
  <head>
  <meta charset="utf-8" />
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">

  <title>测试驱动开发课程</title>
  <meta property="og:title" content="测试驱动开发课程" />
  <meta name="twitter:title" content="测试驱动开发课程" />
  <meta name="description" content="在这个课程中，你将学习如何使用Docker快速创建开发环境、管理多个微服务，应用程序在本地运行后，您将学习怎样部署应用到阿里云ECS实例上，最后我们将扩展ECS上的容器服务。
我们也会练习TDD(测试驱动开发)，在你的项目中测试先行，我们重点将放在服务端的单元测试、功能和集成测试以及端到端的测试上面，以确保整个系统按预期工作。


">
  <meta property="og:description" content="在这个课程中，你将学习如何使用Docker快速创建开发环境、管理多个微服务，应用程序在本地运行后，您将学习怎样部署应用到阿里云ECS实例上，最后我们将扩展ECS上的容器服务。
我们也会练习TDD(测试驱动开发)，在你的项目中测试先行，我们重点将放在服务端的单元测试、功能和集成测试以及端到端的测试上面，以确保整个系统按预期工作。


">
  <meta name="twitter:description" content="在这个课程中，你将学习如何使用Docker快速创建开发环境、管理多个微服务，应用程序在本地运行后，您将学习怎样部署应用到阿里云ECS实例上，最后我们将扩展ECS上的容器服务。
我们也会练习TDD(测试驱动开发)，在你的项目中测试先行，我们重点将放在服务端的单元测试、功能和集成测试以及端到端的测试上面，以确保整个系统按预期工作。


">
  <meta name="author" content=""/>
  <link href='https://blog.qikqiak.com/img/favicon.ico' rel='icon' type='image/x-icon'/>
  <meta property="og:image" content="https://blog.qikqiak.com/img/avatar.jpg" />
  <meta name="twitter:image" content="https://blog.qikqiak.com/img/avatar.jpg" />
  <meta name="twitter:card" content="summary" />
  <meta property="og:url" content="https://blog.qikqiak.com/post/tdd-develop-course/" />
  <meta property="og:type" content="website" />
  <meta property="og:site_name" content="River&#39;s Site" />

  <meta name="generator" content="Hugo 0.24.1" />
  <link rel="canonical" href="https://blog.qikqiak.com/post/tdd-develop-course/" />
  <link rel="alternate" href="https://blog.qikqiak.com/index.xml" type="application/rss+xml" title="River&#39;s Site">
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.7.1/katex.min.css" integrity="sha384-wITovz90syo1dJWVh32uuETPVEtGigN07tkttEqPv+uR2SE/mbQcG7ATL28aI9H0" crossorigin="anonymous">
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" />
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
  <link rel="stylesheet" href="https://blog.qikqiak.com/css/main.css" />
  <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Lora:400,700,400italic,700italic" />
  <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Open+Sans:300italic,400italic,600italic,700italic,800italic,400,300,600,700,800" />
  <link rel="stylesheet" href="https://blog.qikqiak.com/css/pygment_highlights.css" />
  <link rel="stylesheet" href="https://blog.qikqiak.com/css/highlight.min.css" />


<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/photoswipe/4.1.1/photoswipe.min.css" integrity="sha256-sCl5PUOGMLfFYctzDW3MtRib0ctyUvI9Qsmq2wXOeBY=" crossorigin="anonymous" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/photoswipe/4.1.1/default-skin/default-skin.min.css" integrity="sha256-BFeI1V+Vh1Rk37wswuOYn5lsTcaU96hGaI7OUVCLjPc=" crossorigin="anonymous" />



<div class="pswp" tabindex="-1" role="dialog" aria-hidden="true">

<div class="pswp__bg"></div>

<div class="pswp__scroll-wrap">
    
    <div class="pswp__container">
      <div class="pswp__item"></div>
      <div class="pswp__item"></div>
      <div class="pswp__item"></div>
    </div>
    
    <div class="pswp__ui pswp__ui--hidden">
    <div class="pswp__top-bar">
      
      <div class="pswp__counter"></div>
      <button class="pswp__button pswp__button--close" title="Close (Esc)"></button>
      <button class="pswp__button pswp__button--share" title="Share"></button>
      <button class="pswp__button pswp__button--fs" title="Toggle fullscreen"></button>
      <button class="pswp__button pswp__button--zoom" title="Zoom in/out"></button>
      
      
      <div class="pswp__preloader">
        <div class="pswp__preloader__icn">
          <div class="pswp__preloader__cut">
            <div class="pswp__preloader__donut"></div>
          </div>
        </div>
      </div>
    </div>
    <div class="pswp__share-modal pswp__share-modal--hidden pswp__single-tap">
      <div class="pswp__share-tooltip"></div>
    </div>
    <button class="pswp__button pswp__button--arrow--left" title="Previous (arrow left)">
    </button>
    <button class="pswp__button pswp__button--arrow--right" title="Next (arrow right)">
    </button>
    <div class="pswp__caption">
      <div class="pswp__caption__center"></div>
    </div>
    </div>
    </div>
</div>

<script>
window.ga=window.ga||function(){(ga.q=ga.q||[]).push(arguments)};ga.l=+new Date;
ga('create', 'UA-69668147-3', 'auto');
ga('send', 'pageview');
</script>
<script async src='//www.google-analytics.com/analytics.js'></script>

</head>

  <body>
    <nav class="navbar navbar-default navbar-fixed-top navbar-custom">
  <div class="container-fluid">
    <div class="navbar-header">
      <button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#main-navbar">
        <span class="sr-only">Toggle navigation</span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
      </button>
      <a class="navbar-brand" href="https://blog.qikqiak.com/">River&#39;s Site</a>
    </div>

    <div class="collapse navbar-collapse" id="main-navbar">
      <ul class="nav navbar-nav navbar-right">
        
          
            <li>
              <a title="Home" href="https://blog.qikqiak.com/">Home</a>
            </li>
          
        
          
            <li class="navlinks-container">
              <a class="navlinks-parent" href="javascript:void(0)">Categories</a>
              <div class="navlinks-children">
                
                  <a href="https://blog.qikqiak.com/tags/kubernetes">kubernetes</a>
                
                  <a href="https://blog.qikqiak.com/tags/python">python</a>
                
                  <a href="https://blog.qikqiak.com/tags/ops">devops</a>
                
                  <a href="https://blog.qikqiak.com/tags/django">django</a>
                
              </div>
            </li>
          
        
          
            <li class="navlinks-container">
              <a class="navlinks-parent" href="javascript:void(0)">Projects</a>
              <div class="navlinks-children">
                
                  <a href="https://www.haimaxy.com?utm_source=blog&amp;utm_campaign=referral&amp;utm_medium=topmenu">海马学院</a>
                
                  <a href="http://qikqiak.com/play.html">魔多游戏</a>
                
              </div>
            </li>
          
        
          
            <li>
              <a title="Tags" href="https://blog.qikqiak.com/tags">Tags</a>
            </li>
          
        
          
            <li>
              <a title="About" href="https://blog.qikqiak.com/page/about/">About</a>
            </li>
          
        

        

        
      </ul>
    </div>

    <div class="avatar-container">
      <div class="avatar-img-border">
        
          <a title="River&#39;s Site" href="https://blog.qikqiak.com/">
            <img class="avatar-img" src="https://blog.qikqiak.com/img/avatar.jpg" alt="River&#39;s Site" />
          </a>
        
      </div>
    </div>

  </div>
</nav>




    
  
  
  




  
    <div id="header-big-imgs" data-num-img=1 data-img-src-1="https://blog.qikqiak.com/img/posts/23734057_146614452628759_6006637352594702336_n.jpg" data-img-desc-1="Cherish all moments."></div>
  

  <header class="header-section has-img">
    
      <div class="intro-header big-img">
        
        <div class="container">
          <div class="row">
            <div class="col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1">
              <div class="post-heading">
                <h1>测试驱动开发课程</h1>
                  
                  
                    <span class="post-meta">
  Posted on December 28, 2017
  
</span>


                  
              </div>
            </div>
          </div>
        </div>
        <span class="img-desc" style="display: inline;"></span>
      </div>
    
    <div class="intro-header no-img">
      
      <div class="container">
        <div class="row">
          <div class="col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1">
            <div class="post-heading">
              <h1>测试驱动开发课程</h1>
                
                
                  <span class="post-meta">
  Posted on December 28, 2017
  
</span>


                
            </div>
          </div>
        </div>
      </div>
    </div>
  </header>


    
<div class="container" role="main">
  <div class="row">
    <div class="col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1">
      <article role="main" class="blog-post">
        <p>在这个课程中，你将学习如何使用<code>Docker</code>快速创建开发环境、管理多个微服务，应用程序在本地运行后，您将学习怎样部署应用到阿里云ECS实例上，最后我们将扩展ECS上的容器服务。
我们也会练习<code>TDD</code>(测试驱动开发)，在你的项目中测试先行，我们重点将放在服务端的单元测试、功能和集成测试以及端到端的测试上面，以确保整个系统按预期工作。
<img src="https://blog.qikqiak.com/img/posts/flask-tdd-logo-part3.png" alt="flask-tdd-log" /></p>

<p></p>

<h2 id="架构">架构</h2>

<ol>
<li>flask-microservices-main - Docker Compose 文件、Nginx、Admin 脚本</li>
<li>flask-microservices-users - 管理用户和认证的Flask 应用</li>
<li>flask-microservices-client - 客户端，React 应用</li>
<li>flask-microservices-swagger - Swagger API 文档</li>
<li>flask-microservices-eval - 用于管理用户得分和练习的Flask 应用
<img src="https://blog.qikqiak.com/img/posts/testdriven-architecture-part6.png" alt="testdriven-architecture" /></li>
</ol>

<h3 id="第一部分">第一部分</h3>

<h4 id="1-介绍">1. 介绍</h4>

<p>在第一部分，你将能够学习使用<code>Flask</code>、<code>MySQL</code>来创建<code>RESTful</code> API服务，本地运行成功以后，我们将应用部署到阿里云ECS 实例上。</p>

<blockquote>
<p>我们将采取一种实用的方法来进行测试驱动开发(TDD)</p>
</blockquote>

<p>课程开始之前，你应该要熟悉下面的这些主题：
1. Docker - <a href="https://docs.docker.com/engine/getstarted/">Get started with Docker</a>
2. Docker Compose - <a href="https://docs.docker.com/compose/gettingstarted/">Get started with Docker Compose</a>
3. Flask - <a href="https://github.com/mjhea0/flaskr-tdd">Flaskr TDD</a></p>

<p>该部分课程使用到的工具库：
1. Python v3.6.4
2. Flask v0.12.2
3. Flask-Script v2.0.6
4. Flask-SQLAlchemy v2.3.2
5. Flask-Testing v0.7.1
6. PyMySQL v0.8.0
7. Gunicorn v19.7.1
8. Nginx v1.13.0
9. Docker v17.11.0-ce
10. Docker Compose v1.14.0</p>

<h5 id="目标">目标</h5>

<p>本节课程结束，你能学到下面的知识点：
1. 用<code>Flask</code>开发一个<code>RESTful</code> API 服务
2. 实践测试驱动开发(TDD)
3. 在本地用<code>Docker</code>和<code>Docker Compose</code>配置和运行服务
4. 将代码挂载到一个容器中去
5. 在容器中运行单元测试和集成测试
6. 不同容器的服务间的交互
7. 在<code>Docker</code>容器中运行<code>Python</code>和<code>Flask</code>应用
8. 在阿里云ECS 实例上安装<code>Flask</code>、<code>Nginx</code>和<code>Gunicorn</code></p>

<h4 id="2-开始">2. 开始</h4>

<p>本节课程我们将初始化项目框架、定义第一个服务。
创建一个新的项目、安装<code>Flask</code>：</p>

<pre><code class="language-shell">$ mkdir flask-microservices-users &amp;&amp; cd flask-microservices-users
$ mkdir project
$ pyenv virtualenv 3.6.4 tdd3
$ pyenv local tdd3
(tdd3)$ pip install Flask==0.12.2
</code></pre>

<p>在project 目录下面添加<code>__init__.py</code>文件，配置第一个路由：</p>

<pre><code class="language-python"># project/__init__.py
from flask import Flask, jsonify

# 初始化app
app = Flask(__name__)


@app.route('/ping', methods=['GET'])
def ping_pong():
    return jsonify({
        'status': 'success',
        'message': 'pong!'
    })
</code></pre>

<p>然后，添加<code>Flask-Script</code>，该依赖包可以用来在命令行管理<code>Flask</code> app:</p>

<pre><code class="language-shell">(tdd3)$ pip install Flask-Script==2.0.6
</code></pre>

<p>在项目根目录下面添加一个<code>manage.py</code>的文件：</p>

<pre><code class="language-python"># manage.py
from flask_script import Manager
from project import app

manager = Manager(app)

if __name__ == '__main__':
    manager.run()
</code></pre>

<p>这里我们创建了一个新的<code>Manager</code>实例，来处理所有的从命令行输入的命令。运行程序：</p>

<pre><code class="language-shell">(tdd3)$ python manage.py runserver
</code></pre>

<p>运行成功后，我们可以在浏览器中打开页面<code>http://localhost:5000/ping</code>，你能看到如下的json 信息输出到页面：</p>

<pre><code class="language-json">{
  &quot;message&quot;: &quot;pong!&quot;,
  &quot;status&quot;: &quot;success&quot;
}
</code></pre>

<p>然后我们关掉服务(Ctrl+C)，在<code>project</code>目录下面新增一个<code>config.py</code>的文件，内容如下：</p>

<pre><code class="language-python"> #project/config.py
class BaseConfig:
    &quot;&quot;&quot;基础配置&quot;&quot;&quot;
    DEBUG = False
    TESTING = False

class DevelopmentConfig(BaseConfig):
    &quot;&quot;&quot;开发环境配置&quot;&quot;&quot;
    DEBUG = True

class TestingConfig(BaseConfig):
    &quot;&quot;&quot;测试环境配置&quot;&quot;&quot;
    DEBUG = True
    TESTING = True

class ProductionConfig(BaseConfig):
    &quot;&quot;&quot;生产环境配置&quot;&quot;&quot;
    DEBUG = False
</code></pre>

<p>然后更新<code>__init__.py</code>文件，在初始化的时候配置开发环境：</p>

<pre><code class="language-python">from flask import Flask, jsonify

# 初始化app
app = Flask(__name__)
# 环境配置
app.config.from_object('project.config.DevelopmentConfig')

@app.route('/ping', methods=['GET'])
def ping_pong():
    return jsonify({
        'status': 'success',
        'message': 'pong!'
    })
</code></pre>

<p>然后重新运行程序，我们能在终端中看到<code>debug</code>模式的提示信息：</p>

<pre><code class="language-shell">(tdd3)$ python manage.py runserver
    * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
    * Restarting with stat
    * Debugger is active!
    * Debugger PIN: 770-657-842
</code></pre>

<p>现在当你的代码有任何改动的时候，应用都会自动加载，不需要手动重启了。为了保证我们的项目依赖能实时同步，这里我们将依赖包添加到一个<code>requirements.txt</code>的文件中，我们只需要利用<code>pip</code>的<strong>freeze</strong>命令就可以轻松拿到我们项目的依赖列表了：</p>

<pre><code class="language-shell">(tdd3)$ pip freeze &gt; requirements.txt
</code></pre>

<p>然后看看<code>requirements.txt</code>的内容吧:</p>

<pre><code class="language-txt">Flask==0.12.2
Flask-Script==2.0.6
</code></pre>

<p>最后，在项目根目录下面添加一个<code>.gitignore</code>的文件：</p>

<pre><code class="language-txt">__pycache__
env
</code></pre>

<p>初始化git 仓库，然后提交代码吧~</p>

<h3 id="第二部分">第二部分</h3>
      </article>

      <ul class="pager blog-pager">
        
          <li class="previous">
            <a href="https://blog.qikqiak.com/post/alertmanager-of-prometheus-in-practice/" data-toggle="tooltip" data-placement="top" title="Prometheus报警AlertManager实战">&larr; Previous Post</a>
          </li>
        
        
      </ul>

      

      
      <div id="git-comments"></div>
      <link rel="stylesheet" href="https://imsun.github.io/gitment/style/default.css">
      <script src="https://imsun.github.io/gitment/dist/gitment.browser.js"></script>
      <script>
      var gitment = new Gitment({
        owner: 'cnych',
        repo: 'blog',
        oauth: {
          client_id: 'bdb76dbb2e9d0786e350',
          client_secret: 'b454b2a08013fd0e32013be7a63fa8fcb262b6c4',
        }
      })
      gitment.render('git-comments')
      </script>
      
    </div>
  </div>
</div>

    <footer>
  <div class="container">
    <div class="row">
      <div class="col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1">
        <ul class="list-inline text-center footer-links">
          
              <li>
                <a href="mailto:icnych@gmail.com" title="Email me">
                  <span class="fa-stack fa-lg">
                    <i class="fa fa-circle fa-stack-2x"></i>
                    <i class="fa fa-envelope fa-stack-1x fa-inverse"></i>
                  </span>
                </a>
              </li>
          
              <li>
                <a href="https://github.com/cnych" title="GitHub">
                  <span class="fa-stack fa-lg">
                    <i class="fa fa-circle fa-stack-2x"></i>
                    <i class="fa fa-github fa-stack-1x fa-inverse"></i>
                  </span>
                </a>
              </li>
          
              <li>
                <a href="https://weibo.com/cnych" title="微博">
                  <span class="fa-stack fa-lg">
                    <i class="fa fa-circle fa-stack-2x"></i>
                    <i class="fa fa-weibo fa-stack-1x fa-inverse"></i>
                  </span>
                </a>
              </li>
          
              <li>
                <a href="https://instagram.com/cnych" title="Instagram">
                  <span class="fa-stack fa-lg">
                    <i class="fa fa-circle fa-stack-2x"></i>
                    <i class="fa fa-instagram fa-stack-1x fa-inverse"></i>
                  </span>
                </a>
              </li>
          
          
          <li>
            <a href="https://blog.qikqiak.com/index.xml" title="RSS">
              <span class="fa-stack fa-lg">
                <i class="fa fa-circle fa-stack-2x"></i>
                <i class="fa fa-rss fa-stack-1x fa-inverse"></i>
              </span>
            </a>
          </li>
          
        </ul>
        <p class="credits copyright text-muted">
          

          &nbsp;&bull;&nbsp;
          2017

          
            &nbsp;&bull;&nbsp;
            <a href="https://blog.qikqiak.com/">River&#39;s Site</a>
          
        </p>
        
        <p class="credits theme-by text-muted">
          <a href="http://gohugo.io">Hugo v0.24.1</a> powered &nbsp;&bull;&nbsp; Theme by <a href="http://deanattali.com/beautiful-jekyll/">Beautiful Jekyll</a> adapted to <a href="https://github.com/halogenica/beautifulhugo">Beautiful Hugo</a>
          
        </p>
      </div>
    </div>
  </div>
</footer>

<script src="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.7.1/katex.min.js" integrity="sha384-/y1Nn9+QQAipbNQWU65krzJralCnuOasHncUFXGkdwntGeSvQicrYkiUBwsgUqc1" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.7.1/contrib/auto-render.min.js" integrity="sha384-dq1/gEHSxPZQ7DdrM82ID4YVol9BYyU7GbWlIwnwyPzotpoc57wDw/guX8EaYGPx" crossorigin="anonymous"></script>
<script src="https://code.jquery.com/jquery-1.12.4.min.js" integrity="sha256-ZosEbRLbNQzLpnKIkEdrPv7lOy9C27hHQ+Xp8a4MxAQ=" crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
<script src="https://blog.qikqiak.com/js/main.min.js"></script>
<script src="https://blog.qikqiak.com/js/highlight.min.js"></script>
<script> hljs.initHighlightingOnLoad(); </script>
<script> renderMathInElement(document.body); </script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/photoswipe/4.1.1/photoswipe.min.js" integrity="sha256-UplRCs9v4KXVJvVY+p+RSo5Q4ilAUXh7kpjyIP5odyc=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/photoswipe/4.1.1/photoswipe-ui-default.min.js" integrity="sha256-PWHOlUzc96pMc8ThwRIXPn8yH4NOLu42RQ0b9SpnpFk=" crossorigin="anonymous"></script>
<script src="https://blog.qikqiak.com/js/load-photoswipe.min.js"></script>



<script async src="https://www.googletagmanager.com/gtag/js?id=UA-69668147-3"></script>
<script>
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag('js', new Date());
  gtag('config', 'UA-69668147-3');
</script>

  </body>
</html>

